﻿using OxyPlot.Axes;
using OxyPlot.Series;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace OxyPlot.CatExtension
{
    /// <summary>
    /// 使用多核并行计算加速渲染的LineSeries, 不支持BrokenLine，需要x坐标是单调递增
    /// </summary>
    public sealed class FastLineSeries : LineSeries
    {
        private readonly List<ScreenPoint> decimatorBuffer = [];

        private readonly PointsDecimator decimator = new();

        public FastLineSeries() : base()
        {
            IsXMonotonic = true;
        }

        public override void Render(IRenderContext rc)
        {
            var actualPoints = this.ActualPoints;
            if (actualPoints == null || actualPoints.Count == 0)
            {
                return;
            }

            this.VerifyAxes();

            //var sw = Stopwatch.StartNew();
            this.RenderPoints(rc, actualPoints);
            //Debug.WriteLine(sw.ElapsedMilliseconds);

            if (this.LabelFormatString != null)
            {
                // render point labels (not optimized for performance)
                this.RenderPointLabels(rc);
            }

            if (this.LineLegendPosition != LineLegendPosition.None && !string.IsNullOrEmpty(this.Title))
            {
                // renders a legend on the line
                this.RenderLegendOnLine(rc);
            }
        }

        public void RenderPoints(IRenderContext rc, List<DataPoint> points)
        {
            var (start, end) = this.GetScreenDataRange(points);

            decimator.Decimate(points, start, end, XAxis.Offset, XAxis.Scale, YAxis.Offset, YAxis.Scale, this.decimatorBuffer);

            this.RenderLineAndMarkers(rc, this.decimatorBuffer);
            decimatorBuffer.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private (int, int) GetScreenDataRange(List<DataPoint> points)
        {
            var span = CollectionsMarshal.AsSpan(points);

            var xmin = this.XAxis.ClipMinimum;
            var xmax = this.XAxis.ClipMaximum;

            var start = BinarySearch(span, xmin);
            var end = BinarySearch(span, xmax);

            return (Math.Max(start - 1, 0), Math.Min(end + 1, points.Count - 1));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int BinarySearch(Span<DataPoint> points, double x)
        {
            int left = 0, mid = 0, right = points.Length - 1;

            if (x <= points[left].X)
            {
                return left;
            }

            if (points[right].X <= x)
            {
                return right;
            }

            while (left <= right)
            {
                mid = (left + right) / 2;
                var result = x - points[mid].X;

                if (result < 0)
                {
                    right = mid - 1;
                }
                else
                {
                    left = mid + 1;
                }
            }
            return mid;
        }

        private void RenderLineAndMarkers(IRenderContext rc, List<ScreenPoint> pointsToRender)
        {
            var screenPoints = pointsToRender;

            if (this.StrokeThickness > 0 && this.ActualLineStyle != LineStyle.None)
            {
                this.RenderLine(rc, screenPoints);
            }

            if (this.MarkerType != MarkerType.None)
            {
                var markerBinOffset = this.MarkerResolution > 0 ? this.Transform(this.MinX, this.MinY) : default(ScreenPoint);

                rc.DrawMarkers(
                    pointsToRender,
                    this.MarkerType,
                    this.MarkerOutline,
                    [this.MarkerSize],
                    this.ActualMarkerFill,
                    this.MarkerStroke,
                    this.MarkerStrokeThickness,
                    this.EdgeRenderingMode,
                    this.MarkerResolution,
                    markerBinOffset);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void RenderLine(IRenderContext rc, List<ScreenPoint> pointsToRender)
        {
            var dashArray = this.ActualDashArray;

            DrawReducedLine(
                rc,
                pointsToRender,
                this.GetSelectableColor(this.ActualColor),
                this.StrokeThickness,
                this.EdgeRenderingMode,
                dashArray,
                this.LineJoin);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void DrawReducedLine(IRenderContext rc, List<ScreenPoint> points, OxyColor stroke, double strokeThickness, EdgeRenderingMode edgeRenderingMode, double[]? dashArray, LineJoin lineJoin)
        {
            rc.DrawLine(points, stroke, strokeThickness, edgeRenderingMode, dashArray, lineJoin);
        }

        public override TrackerHitResult? GetNearestPoint(ScreenPoint point, bool interpolate)
        {
            if (XAxis == null || YAxis == null || ActualPoints == null || ActualPoints.Count == 0)
            {
                return null;
            }

            TrackerHitResult? result;
            if (interpolate)
            {
                result = GetNearestInterpolatedPoint(point);
            }
            else
            {
                result = GetNearestPoint(point);
            }

            if (result != null)
            {
                result.Text = StringHelper.Format(
                    this.ActualCulture,
                    this.TrackerFormatString,
                    result.Item,
                    this.Title,
                    this.XAxis.Title ?? XYAxisSeries.DefaultXAxisTitle,
                    this.XAxis.GetValue(result.DataPoint.X),
                    this.YAxis.Title ?? XYAxisSeries.DefaultYAxisTitle,
                    this.YAxis.GetValue(result.DataPoint.Y));
            }

            return result;

        }

        private TrackerHitResult? GetNearestInterpolatedPoint(ScreenPoint screenPoint)
        {
            var dataPoint = InverseTransform(screenPoint);

            var span = CollectionsMarshal.AsSpan(ActualPoints);

            var p1Index = BinarySearch(span, dataPoint.X);

            var p2Index = 0;

            if (p1Index == 0 || p1Index == span.Length - 1)
            {
                p2Index = p1Index;
            }
            else if (span[p1Index].X < dataPoint.X)
            {
                p2Index = p1Index + 1;
            }
            else
            {
                p2Index = p1Index - 1;
            }

            double dx, dy;

            if (p1Index == p2Index)
            {
                dx = span[p1Index].X;
                dy = span[p1Index].Y;
            }
            else
            {
                var k1 = (span[p2Index].Y - span[p1Index].Y) / (span[p2Index].X - span[p1Index].X);
                var b1 = span[p1Index].Y - k1 * span[p1Index].X;

                var k2 = -1 / k1;
                var b2 = dataPoint.Y - k2 * dataPoint.X;

                dx = (b2 - b1) / (k1 - k2);
                dy = k1 * dx + b1;
            }

            dataPoint = new DataPoint(dx, dy);
            screenPoint = Transform(dataPoint);

            var index = Math.Min(p1Index, p2Index);

            return new TrackerHitResult
            {
                Series = this,
                DataPoint = dataPoint,
                Position = screenPoint,
                Index = index,
                Item = span[index],
            };
        }

        private TrackerHitResult? GetNearestPoint(ScreenPoint screenPoint)
        {
            var dataPoint = InverseTransform(screenPoint);

            var span = CollectionsMarshal.AsSpan(ActualPoints);

            var p1Index = BinarySearch(span, dataPoint.X);

            var p2Index = 0;

            if (p1Index == 0 || p1Index == span.Length - 1)
            {
                p2Index = p1Index;
            }
            else if (span[p1Index].X < dataPoint.X)
            {
                p2Index = p1Index + 1;
            }
            else
            {
                p2Index = p1Index - 1;
            }

            var index = Math.Min(p1Index, p2Index);

            dataPoint = span[index];
            screenPoint = Transform(dataPoint);

            return new TrackerHitResult
            {
                Series = this,
                DataPoint = dataPoint,
                Position = screenPoint,
                Index = index,
                Item = dataPoint
            };
        }
    }
}
